#include <zenocli/main.hpp>
#include <zenox/minilog.hpp>
#include <zenox/cppdemangle.hpp>
#include <zenox/localization.hpp>
#include <zenox/subprocess.hpp>
#include <zenox/unicode.hpp>
#include <zenox/home.hpp>
#include <sstream>
#include <cstdlib>
#include <csignal>
#include <locale>
#include <ctime>
#if defined(ZENO_HAS_BACKWARD_CPP)
#include <backward.hpp>
#endif
#if defined(_WIN32)
#include <windows.h>
#ifndef CP_UTF8
#define CP_UTF8 65001
#endif
#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
#endif
#ifndef DISABLE_NEWLINE_AUTO_RETURN
#define DISABLE_NEWLINE_AUTO_RETURN 0x0008
#endif
#ifndef ENABLE_VIRTUAL_TERMINAL_INPUT
#define ENABLE_VIRTUAL_TERMINAL_INPUT 0x0020
#endif
#elif defined(__linux__)
#include <signal.h>
#endif

#if defined(_WIN32)
static std::wstring winmbtowc(std::string_view in) {
    std::wstring out;
    out.resize(MultiByteToWideChar(CP_UTF8, 0, in.data(), in.size(), NULL, 0));
    MultiByteToWideChar(CP_UTF8, 0, in.data(), in.size(), out.data(), out.size());
    return out;
}

static bool enable_windows_vterm() {
    HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
    if (hOut == INVALID_HANDLE_VALUE)
    {
        return false;
    }
    HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
    if (hIn == INVALID_HANDLE_VALUE)
    {
        return false;
    }

    static DWORD dwOriginalOutMode = 0;
    static DWORD dwOriginalInMode = 0;
    if (!GetConsoleMode(hOut, &dwOriginalOutMode))
    {
        return false;
    }
    if (!GetConsoleMode(hIn, &dwOriginalInMode))
    {
        return false;
    }

    DWORD dwRequestedOutModes = ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
    DWORD dwRequestedInModes = ENABLE_VIRTUAL_TERMINAL_INPUT;

    DWORD dwOutMode = dwOriginalOutMode | dwRequestedOutModes;
    if (!SetConsoleMode(hOut, dwOutMode))
    {
        // we failed to set both modes, try to step down mode gracefully.
        dwRequestedOutModes = ENABLE_VIRTUAL_TERMINAL_PROCESSING;
        dwOutMode = dwOriginalOutMode | dwRequestedOutModes;
        if (!SetConsoleMode(hOut, dwOutMode))
        {
            // Failed to set any VT mode, can't do anything here.
            return false;
        }
    }
    std::atexit(+[] {
        SetConsoleMode(hOut, dwOriginalOutMode);
    });

    DWORD dwInMode = dwOriginalInMode | dwRequestedInModes;
    if (!SetConsoleMode(hIn, dwInMode))
    {
        // Failed to set VT input mode, can't do anything here.
        return false;
    }
    std::atexit(+[] {
        SetConsoleMode(hIn, dwOriginalInMode);
    });

    return true;
}

static void cihou_windows_console() {
    // set console code page to CP_UTF8, equivalent to std::system("chcp 65001")
    // this combined with /utf-8 option specified in cmake, will prevents LuanMa in console
    static UINT oldCCP = GetConsoleOutputCP();
    if (!SetConsoleOutputCP(CP_UTF8)) {
        std::cerr << "warning: failed to chcp 65001 for utf-8 output\n";
    } else {
        std::atexit(+[] {
            SetConsoleOutputCP(oldCCP);
        });
    }
    static UINT oldCP = GetConsoleCP();
    if (!SetConsoleCP(CP_UTF8)) {
        std::cerr << "warning: failed to chcp 65001 for utf-8 input\n";
    } else {
        std::atexit(+[] {
            SetConsoleCP(oldCP);
        });
    }
    // enable 'virtual terminal' aka ANSI control sequence support, for color output via "\x1b[31;1m"
    if (!enable_windows_vterm()) {
        std::cerr << "warning: failed to enable ansi color output\n";
    }
#ifdef NDEBUG // try hide the console window in release mode
    if (!std::getenv("ZENO_SHOW_WIN_CONSOLE")) {
        if (DWORD procId = 0, count = GetConsoleProcessList(&procId, 1); count < 2) {
            zenox::log_debug("running in GUI mode, hiding console now");
            if (HWND hwnd = GetConsoleWindow(); hwnd != NULL && hwnd != INVALID_HANDLE_VALUE) {
                ShowWindow(hwnd, SW_HIDE);
            }
        }
    }
#endif
}
#endif

#ifdef _WIN32
static DWORD winsetclipboard(LPCWSTR str) {
    DWORD len = wcslen(str);
    HGLOBAL hdst;
    LPWSTR dst;
    // Allocate string for cwd
    hdst = GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, ((SIZE_T)len + 1) * sizeof(WCHAR));
    if (hdst == NULL) return GetLastError();
    dst = (LPWSTR)GlobalLock(hdst);
    if (dst == NULL) return GetLastError();
    memcpy(dst, str, len * sizeof(WCHAR));
    dst[len] = 0;
    GlobalUnlock(hdst);
    // Set clipboard data
    if (!OpenClipboard(NULL)) return GetLastError();
    EmptyClipboard();
    if (!SetClipboardData(CF_UNICODETEXT, hdst)) return GetLastError();
    CloseClipboard();
    return 0;
}
static void winsetclipboard(std::string const &str) {
    winsetclipboard(winmbtowc(str).c_str());
}
#endif

static void popup_message_box(std::string caption, std::string message, bool is_error) {
#if defined(_WIN32)
    if (is_error) {
        winsetclipboard(message);
        message += '\n';
        message += "(error message copied to clipboard)"_zenos;
    }
    MessageBoxW(NULL,
                winmbtowc(message).c_str(),
                winmbtowc(caption).c_str(),
                MB_OK | (is_error ? MB_ICONERROR : MB_ICONINFORMATION));
#elif defined(__linux__)
    zenox::subprocess("notify-send")
        .arg("--app-name=Zeno")
        .arg(is_error ? "--urgency=critical" : "--urgency=low")
        .argpath("--icon=", zenox::zeno_home_relative("assets/logo.png"))
        .arg(caption)
        .arg(message)
        .detach()
        ;
    /* std::ostringstream oss; */
    /* oss << "notify-send --app-name='Zeno' "; */
    /* if (is_error) */
    /*     oss << "--urgency=critical "; */
    /* else */
    /*     oss << "--urgency=low "; */
    /* oss << std::quoted("--icon=" + zenox::u8path(zenox::zeno_home_relative("assets/logo.png")).string(), '\'', '\\'); */
    /* oss << ' '; */
    /* oss << std::quoted(caption, '\'', '\\'); */
    /* oss << ' '; */
    /* std::replace(message.begin(), message.end(), '\n', ' '); */
    /* std::replace(message.begin(), message.end(), '\'', '"'); */
    /* oss << std::quoted(message, '\'', '\\'); */
    /* (void)std::system(oss.str().c_str()); */
#else
    std::cerr << caption << '\n' << message << '\n';
#endif
}

static std::string get_datestamp_log_file_name() {
    static char datestamp[64];
    datestamp[std::strftime(datestamp, 63, "zeno_minilog_%Y-%m-%d.log", std::localtime(&(time_t const &)std::time(nullptr)))] = 0;
    /* return zenox::zeno_home_relative(datestamp); */
    return zenox::pathu8(std::filesystem::temp_directory_path() / zenox::u8path(datestamp));
}

#if defined(ZENO_HAS_BACKWARD_CPP)
ZENO_NOINLINE static std::string dump_stack_trace(std::size_t skip = 1, std::size_t count = 32) {
#ifdef _WIN32
    static int _win_first_shot_bug_fixer = [] {
        backward::StackTrace tb;
        tb.load_here(4);
        backward::TraceResolver tr;
        tr.load_stacktrace(tb);
        if (tb.size()) {
            volatile backward::ResolvedTrace trace = tr.resolve(tb[0]);
        }
        return 0;
    }();
#endif
    backward::StackTrace tb;
    tb.load_here(count + skip);
    backward::TraceResolver tr;
    tr.load_stacktrace(tb);
    std::ostringstream oss;
    oss << "stack back trace (most recent call first):\n";
    for (size_t i = skip; i < tb.size(); ++i) {
        backward::ResolvedTrace trace = tr.resolve(tb[i]);
        oss << "#" << i - skip;
        if (!trace.object_filename.empty()) {
            oss << " [" << zenox::zeno_source_dir_prefix_remove(trace.object_filename) << "]";
        }
        if (!trace.object_function.empty()) {
            oss << " " << trace.object_function;
        }
        if (!trace.source.filename.empty()) {
            oss << " (" << zenox::zeno_source_dir_prefix_remove(trace.source.filename);
            oss << ":" << trace.source.line << ")";
        }
        if (trace.addr) {
            oss << " <" << trace.addr << ">";
        }
        oss << " \n"[i + 1 != tb.size()];
    }
    return oss.str();
}
#endif

ZENO_COLD_FN static void popup_message_box(const char *message) {
    auto msg = zenox::format("Please consider submit the file {} to https://zenustech.com/bugreport to help us improve, thank you! Below is the original error message: {}"_zenosv, get_datestamp_log_file_name(), message);
#if defined(ZENO_HAS_BACKWARD_CPP)
    msg += "\n------------\n" + dump_stack_trace();
#endif
    popup_message_box("Zeno Crashed"_zenos, msg, true);
}

ZENO_COLD_FN [[noreturn]] static void flush_log_and_abort(int status) {
    zenox::logger::default_logger().flush_log();
    std::_Exit(status);
}

ZENO_COLD_FN static void on_terminate_handler() {
#if defined(ZENO_HAS_BACKWARD_CPP)
    zenox::log_critical("{}", dump_stack_trace());
#endif
    try {
        if (auto ep = std::current_exception()) [[likely]]
            std::rethrow_exception(ep);
    } catch (zenox::minilog::details::fatal_exception const &e) {
        zenox::log_fatal("fatal error occurred, program exiting");
        popup_message_box(e.what());
        flush_log_and_abort(1);
    } catch (std::exception const &e) {
        zenox::log_fatal("uncaught exception occurred [{}]\n============\n{}\n============\nprogram exiting", zenox::cppdemangle(e), e.what());
        popup_message_box(e.what());
        flush_log_and_abort(2);
    } catch (...) {
        zenox::log_fatal("non standard type of exception occurred, program exiting");
        popup_message_box("non standard type of exception occurred");
        flush_log_and_abort(3);
    }
    zenox::log_fatal("terminated without an active exception, program exiting");
    popup_message_box("terminated without an active exception");
    flush_log_and_abort(4);
}

ZENO_COLD_FN static void on_signal_handler(int sig) {
    std::signal(sig, SIG_DFL);
#if defined(ZENO_HAS_BACKWARD_CPP)
    zenox::log_critical("{}", dump_stack_trace());
#endif
#if defined(__linux__)
    zenox::log_fatal("terminated due to signal {}: {}", sig, strsignal(sig));
    popup_message_box(zenox::format("terminated due to signal {}: {}", sig, strsignal(sig)).c_str());
#else
    zenox::log_fatal("terminated due to signal {}", sig);
    popup_message_box(zenox::format("terminated due to signal {}", sig).c_str());
#endif
    flush_log_and_abort(-sig);
}

#if defined(__linux__)
ZENO_COLD_FN static void on_segfault_handler(int sig, siginfo_t *info, void *) {
    std::signal(sig, SIG_DFL);
    void *fault_address = (void *)info->si_addr;
    zenox::log_error("segmentation fault at address [{}]", fault_address);
    zenox::logger::default_logger().flush_log();
#if defined(ZENO_HAS_BACKWARD_CPP)
    zenox::log_critical("{}", dump_stack_trace());
#endif
    zenox::log_fatal("terminated due to signal {}: segmentation fault at address [{}]", sig, fault_address);
    popup_message_box(zenox::format("terminated due to signal {}: segmentation fault at address [{}]", sig, fault_address).c_str());
    flush_log_and_abort(-sig);
}
#endif

int main(int argc, char **argv) {
    std::ios::sync_with_stdio(false);
#if defined(_WIN32)
#ifdef ZENO_HAS_SPLASH
    if (!std::getenv("ZENO_NO_SPLASH_SCREEN")) (void)LoadLibraryW(L"splashw.dll");
#endif
    try {
        // https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setlocale-wsetlocale
        std::setlocale(LC_ALL, ".UTF-8");
    } catch (...) {
        std::cerr << "warning: failed to set utf-8 locale\n";
    }
    cihou_windows_console();
#elif defined(__linux__)
#ifdef ZENO_HAS_SPLASH
    if (!std::getenv("ZENO_NO_SPLASH_SCREEN")) (void)dlopen("libsplashw1z.so", RTLD_LAZY);
#endif
    std::setlocale(LC_ALL, "C");
#endif

    std::set_terminate(on_terminate_handler);
    std::signal(SIGFPE, on_signal_handler);
    std::signal(SIGILL, on_signal_handler);
    std::signal(SIGTERM, on_signal_handler);
    std::signal(SIGABRT, on_signal_handler);
#if defined(__linux__)
    struct sigaction act = {};
    act.sa_sigaction = on_segfault_handler;
    act.sa_flags = SA_SIGINFO;
    sigaction(SIGSEGV, &act, NULL);
#else
    std::signal(SIGSEGV, on_signal_handler);
#endif
#ifdef SIGILL
    std::signal(SIGILL, on_signal_handler);
#endif
#ifdef SIGPIPE
    std::signal(SIGPIPE, on_signal_handler);
#endif

    zenox::logger::default_logger().set_log_file(zenox::touch_directory_if_not_exist(get_datestamp_log_file_name()));
    zenox::log_info("zeno started version {} home [{}]", zenox::zeno_build_date(), zenox::zeno_home());
#ifndef NDEBUG
    zenox::log_warn("this is a debug build, performace may be degraded");
#endif

    int ret = zenocli::main(argc, argv);
    if (ret != 0) [[unlikely]]
        zenox::log_warn("program exiting with {}", ret);
    return ret;
}
